// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package elastic

import (
	"context"
	"encoding/json"
	"fmt"
	"gitee.com/go-wares/log/base"
	"gitee.com/go-wares/log/conf"
	"github.com/elastic/go-elasticsearch/v7"
	"github.com/elastic/go-elasticsearch/v7/esapi"
	"io"
	"strings"
	"sync"
	"time"
)

type (
	// Adapter
	// is a component that print log message on terminal.
	Adapter struct {
		bucket    *base.Bucket[*base.Item]
		client    *elasticsearch.Client
		formatter base.LoggerFormatter
		mu        *sync.Mutex
	}

	ese struct {
		Error *struct {
			Type   string `json:"type"`
			Reason string `json:"reason"`
		} `json:"error"`
	}
)

// NewAdapter
// creates a new adapter for terminal.
func NewAdapter() *Adapter {
	return (&Adapter{
		formatter: &Formatter{},
	}).init()
}

// GetBucket
// return logger items bucket.
func (o *Adapter) GetBucket() *base.Bucket[*base.Item] {
	return o.bucket
}

// GetFormatter
// returns the logger formatter.
func (o *Adapter) GetFormatter() base.LoggerFormatter {
	return o.formatter
}

// Log
// add logger item into bucket.
func (o *Adapter) Log(item *base.Item) {
	o.bucket.Add(item)
}

// SetFormatter
// set the logger formatter.
func (o *Adapter) SetFormatter(formatter base.LoggerFormatter) {
	o.formatter = formatter
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *Adapter) getClient() (*elasticsearch.Client, error) {
	var (
		c   = conf.Get()
		cli *elasticsearch.Client
		err error
	)
	if o.client == nil {
		if cli, err = elasticsearch.NewClient(elasticsearch.Config{
			Addresses: c.ElasticAdapter.Addresses,
			Username:  c.ElasticAdapter.Username,
			Password:  c.ElasticAdapter.Password,
		}); err == nil {
			o.client = cli
		}
	}
	return o.client, err
}

func (o *Adapter) init() *Adapter {
	o.mu = &sync.Mutex{}
	o.bucket = base.NewBucket[*base.Item]("elastic-logger", o.run)
	o.bucket.Start()
	return o
}

func (o *Adapter) run(items []*base.Item) {
	var (
		body   []byte
		c      = conf.Get()
		cli    *elasticsearch.Client
		ctx, _ = context.WithTimeout(context.Background(), time.Duration(c.ElasticAdapter.Timeout)*time.Second)
		data   strings.Builder
		err    error
		ln     byte = '\n'
		res    *esapi.Response
	)

	o.mu.Lock()
	defer o.mu.Unlock()

	// Release
	// log items when write completed.
	defer func() {
		// Catch
		// runtime panic.
		if r := recover(); r != nil {
		}

		// Release items.
		for _, item := range items {
			item.Release()
		}

		// Print error message.
		if err != nil {
			println("send messages fail:", err.Error())
		}
	}()

	// Render log messages.
	for i, item := range items {
		if ib := o.formatter.Format(item); ib != nil {
			if ii := (&Indices{}).Format(item, i); ii != nil {
				// Add
				// indices info.
				data.Write(ii)
				data.WriteByte(ln)
				// Add
				// indices document
				data.Write(ib)
				data.WriteByte(ln)
			}
		}
	}

	// Client
	// create and send log messages.
	if cli, err = o.getClient(); err == nil {
		if res, err = (&esapi.BulkRequest{
			Body: strings.NewReader(data.String()),
		}).Do(ctx, cli); err != nil {
			return
		}
	}

	// Close body when ended.
	defer func() {
		_ = res.Body.Close()
	}()

	// Read response results.
	if body, err = io.ReadAll(res.Body); err != nil {
		return
	}

	// Result error checks
	ptr := &ese{}
	if json.Unmarshal(body, ptr) == nil && ptr.Error != nil {
		err = fmt.Errorf("%s - %s", ptr.Error.Type, ptr.Error.Reason)
	}
}
